Skip to content

Qwen3-14B serving: add device top-k sampling - #77

Merged
superxf merged 1 commit into
hw-native-sys:mainfrom
zmnobug:topk-sampling-optimization
Jul 31, 2026
Merged

Qwen3-14B serving: add device top-k sampling#77
superxf merged 1 commit into
hw-native-sys:mainfrom
zmnobug:topk-sampling-optimization

Conversation

@zmnobug

@zmnobug zmnobug commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Extend the Qwen3-14B device sampling path introduced by #47 to support non-greedy top-k sampling.

For supported requests, a standalone NPU operator reduces full-vocabulary logits to a fixed-width candidate set. The existing host sampler consumes only the candidate values and token IDs, then applies temperature, top-p filtering, distribution validation, and multinomial sampling.

Closes #74.

Paired exact-selector pypto-lib PR: hw-native-sys/pypto-lib#787

What Changed

  • Add an executor capability for the maximum device top-k width.
  • Add SamplingCandidates to prefill/decode result contracts.
  • Automatically enable device candidate selection when temperature > 0 and 0 < top_k <= 32.
  • Preserve the existing CPU full-logits fallback for unsupported configurations.
  • Compile the Qwen3 selector as a separate L3 callable.
  • Reuse one selector for prefill greedy top-1 and non-greedy top-k candidate selection; decode greedy remains in the decode path introduced by Qwen3-14B serving: device-side embedding and sampling #47.
  • Allocate reusable prefill/decode candidate buffers and return only active rows.
  • Reuse the existing sampler for temperature, top-p, distribution validation, and multinomial selection.
  • Wire both offline generation and the serving worker through the same capability-based path.
  • Add routing, fallback, candidate sampling, row validation, and operator integration tests.

Scope

  • Qwen3-14B on the NPU backend.
  • Fixed device candidate capacity of 32; requests with a larger top_k use the CPU fallback.
  • The selector remains a standalone operator and is not fused into prefill or decode.
  • Final probabilistic sampling remains on the host.

Performance

Final exact top-k: five-run feature comparison

Measured on device 0 with /data/models/Qwen3-14B, prompt 北京故宫是, 128 generated tokens, temperature=0.8, top_k=32, and top_p=1.0. Model initialization and kernel compilation are excluded.

The benchmark isolated the feature before the latest rebase:

  • CPU top-k baseline: the measured feature commit's exact parent, pypto-serving@e57e14f.
  • Exact NPU top-k: the measured feature commit, pypto-serving@3658ed8.
  • Both revisions use pypto-lib@7e7d4cc, the same zm_pypto environment, the same simpler wheel, PTO-ISA checkout, ptoas v0.4.8, model files, and device.
  • Baseline and current runs were interleaved on the same device, five runs per revision.

The PR is now rebased as pypto-serving@553252f on upstream/main@1ebd330. The rebase retained the same top-k implementation and only resolved the upstream Qwen BATCH/BATCH_PAD compatibility change; the performance table records the measured pre-rebase revisions above.

Metric CPU top-k baseline Exact NPU top-k Change
Generate E2E, mean 14.074 s 4.401 s -68.7%
Generate E2E, median 9.730 s 4.406 s -54.7%
E2E throughput, mean 10.74 tok/s 29.09 tok/s +170.9%, 2.71x
E2E throughput, median 13.15 tok/s 29.05 tok/s +120.9%, 2.21x
Prefill / TTFT, mean 263.0 ms 270.6 ms +7.6 ms
Decode API, mean 34.12 ms/token 31.96 ms/token -6.3%
Time outside prefill/decode APIs, mean 9.477 s 0.072 s -99.2%

The individual 128-token E2E runs were:

  • CPU top-k baseline: 16.685, 25.575, 8.991, 9.391, and 9.730 s.
  • Exact NPU top-k: 4.385, 4.406, 4.444, 4.488, and 4.281 s.

The CPU path has substantial host-side variance, so both mean and median are reported. Even the conservative median comparison reduces E2E latency by 54.7% and improves throughput by 2.21x. The NPU path is also stable across the five runs, with a 4.281-4.488 s E2E range.

The gain primarily comes from keeping decode full-vocabulary logits device-resident and returning only 32 candidate values and token IDs per active row. Decode kernel time changes only modestly; the dominant full-logits D2H and host full-vocabulary top-k work outside the executor APIs is removed.

Exactness Design

The follow-up exact implementation replaces the original per-512-chunk Top-4 approximation with a provably exact hierarchical reduction:

  1. Split 151936 real-vocabulary logits into 74 full 2048-token groups and one 384-token tail group.
  2. Compute an exact Top-32 for every group using sort32 and mrgsort.
  3. Merge the resulting 75 x 32 = 2400 candidates in a 4096-entry padded buffer and select the exact global Top-32.
  4. Compare the NPU output directly against torch.topk(logits[:, :REAL_VOCAB], 32).

The proof is straightforward: if an entry is not in its group's Top-32, at least 32 entries in the same group rank ahead of it, so it cannot belong to the global Top-32. Therefore the union of exact group Top-32 sets contains the exact global Top-32.

The test fixture covers both adversarial distributions:

  • All 32 largest values concentrated in one 512-token region, which fails the original Top-4 approximation.
  • One of the 32 largest values placed in each of 32 different chunks, which checks token-ID propagation.

The 2048-token grouping also reduces the generated sorting/merge task graph compared with computing Top-32 independently for all 297 512-token chunks. The exact implementation passed the shared-L3 batch-16 warmup and full prefill/decode generation without the 507018 timeout seen with larger task-graph prototypes.

Data Movement Note

For supported decode requests, the decode callable writes full-vocabulary logits to a device scratch tensor, and the standalone top-k selector reads that device tensor directly. DecodeResult.logits is None; only 32 candidate values and 32 token IDs per active row are copied back for the existing host probability sampler.

Prefill still retains its full-logits result contract for fallback. Unsupported configurations, including top_k > 32, continue to use the existing CPU full-logits path.

Validation

  • Exact Top-32 NPU output passed direct full-vocabulary torch.topk comparison.
  • Greedy selection_k=1 regression passed host argmax comparison.
  • Focused rebase validation in zm_pypto: 9 top-k/Qwen routing and compile-interface tests passed.
  • tests/test_device_sampling_submission.py: 5 passed after rebase.
  • git diff --check: passed in serving and pypto-lib.
  • Shared-L3 batch-16 prefill/decode warmup: passed.
  • Rebased standalone selector, 100 rounds: 428.0 us mean.
  • Five interleaved 128-token exact-parent A/B runs completed successfully on device 0.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Qwen3 device sampling now uses a shared top-k selector kernel, exposes candidate tensors through executor results, and performs final temperature/top-p sampling on the host. Capability checks preserve host fallback behavior for unsupported configurations.

Changes

Device top-k sampling

Layer / File(s) Summary
Sampling contracts and fallback flow
python/core/types.py, python/core/executor.py, python/core/engine.py, python/core/serving_worker.py, python/core/sampler.py
Adds SamplingCandidates, executor capability detection, batch flags, candidate-based sampling, and host fallback logic.
Qwen3 top-k kernel compilation
examples/model/qwen3_14b/runner/npu_executor.py, examples/model/qwen3_14b/runner/qwen3_l3_dispatch.py
Replaces greedy-kernel compilation and wiring with topk_select, including fixed-shape validation and top-k buffers.
Qwen3 runner execution integration
examples/model/qwen3_14b/runner/npu_runner.py
Runs selector-based greedy and top-k paths, returns candidates from prefill/decode, shares new buffers, and dispatches the top-k worker program.
Kernel and batching validation
tests/test_batching.py, tests/test_device_sampling_submission.py, pypto-lib
Updates test doubles and assertions for candidate sampling, runtime vocabulary limits, tie-breaking, and top-k kernel wiring; advances the library submodule reference.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant ServingWorker
  participant QwenRunner
  participant TopKKernel
  participant Sampler
  Request->>ServingWorker: submit top-k generation request
  ServingWorker->>QwenRunner: run prefill/decode with top-k enabled
  QwenRunner->>TopKKernel: dispatch topk_select
  TopKKernel-->>QwenRunner: return candidate values and token IDs
  QwenRunner-->>ServingWorker: return sampling_candidates
  ServingWorker->>Sampler: sample_from_candidates
  Sampler-->>ServingWorker: selected token
Loading

Poem

I’m a rabbit with kernels to hop,
Sorting bright candidates right to the top.
The sampler picks with a whisker-soft gleam,
While fallback paths guard the stream.
New buffers bloom in the NPU dream.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes add device top-k selection, candidate propagation, capability gating, and CPU fallback as requested by #74.
Out of Scope Changes check ✅ Passed The modified files and tests are all scoped to Qwen3-14B device top-k sampling and its supporting runtime wiring.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly identifies the Qwen3-14B serving change and the addition of device top-k sampling.
Description check ✅ Passed The description directly explains the device top-k sampling implementation, fallback behavior, performance, scope, and validation.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for device-side top-k candidate selection and sampling for the Qwen3-14B model on NPU. It adds the necessary JIT compilation, host dispatching, and buffer allocations for the topk_select kernel. Additionally, it updates the core engine, serving worker, and sampler to support retrieving and sampling from these device-provided top-k candidates when configured. Unit tests are also added to verify the correctness of the top-k sampling path. Feedback suggests adding a boundary check for row_idx in sample_from_candidates to prevent potential out-of-bounds errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread python/core/sampler.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
examples/model/qwen3_14b/runner/qwen3_l3_dispatch.py (1)

137-144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the stale qwen3_greedy_sample_host wrapper examples/model/qwen3_14b/runner/qwen3_l3_dispatch.py:137-144. The runtime now routes greedy prefill through _run_sampling_selector(..., selection_k=1) and only wires topk_select_fwd, so this shim still dereferences greedy_sample_fwd even though nothing assigns it. Update the batching test that uses this symbol as a slice marker if you drop it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/model/qwen3_14b/runner/qwen3_l3_dispatch.py` around lines 137 - 144,
Remove the unused qwen3_greedy_sample_host wrapper and any associated stale
greedy_sample_fwd reference from the dispatch module. Update the batching test’s
slice marker to use the next valid symbol or boundary so it remains correct
after the wrapper is deleted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@examples/model/qwen3_14b/runner/qwen3_l3_dispatch.py`:
- Around line 137-144: Remove the unused qwen3_greedy_sample_host wrapper and
any associated stale greedy_sample_fwd reference from the dispatch module.
Update the batching test’s slice marker to use the next valid symbol or boundary
so it remains correct after the wrapper is deleted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d08f065-eb24-4d7a-9795-94d920e769c3

📥 Commits

Reviewing files that changed from the base of the PR and between 49297c2 and f7d2775.

📒 Files selected for processing (11)
  • examples/model/qwen3_14b/runner/npu_executor.py
  • examples/model/qwen3_14b/runner/npu_runner.py
  • examples/model/qwen3_14b/runner/qwen3_l3_dispatch.py
  • pypto-lib
  • python/core/engine.py
  • python/core/executor.py
  • python/core/sampler.py
  • python/core/serving_worker.py
  • python/core/types.py
  • tests/test_batching.py
  • tests/test_device_sampling_submission.py

@zmnobug

zmnobug commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the automated review feedback in the latest commits:\n\n- Added candidate row-count and row_idx bounds validation with negative/out-of-range tests in e23603f.\n- Removed the stale qwen3_greedy_sample_host wrapper and unused greedy_sample_fwd reference in cbf4e75.\n\nValidation: 28 focused tests passed and Ruff checks passed.

@zmnobug

zmnobug commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

CI OOM follow-up: commit 6b3fe35 reduces the two Qwen3 CI guards from a 1 GiB to a 512 MiB PTO2_RING_HEAP. The runtime reserves roughly four heaps, so this releases about 2 GiB for the 40-layer model; the failed 7,130,316,800-byte allocation is the stacked BF16 FFN weight, not a sampling buffer.\n\nLocal validation with the new CI values completed static weight upload, prefill/decode warmup, KV allocation, and generation successfully (65.79 GB device: 33.64 GB peak non-KV, 2.28 GB arena). The new workflow run is waiting for maintainer approval: https://github.com/hw-native-sys/pypto-serving/actions/runs/29303581069

@zmnobug

zmnobug commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Local CI-equivalent validation passed with zm_pypto and the new 512 MiB ring heap configuration.\n\n- Test: tests/test_qwen3_accuracy.py\n- Result: 1 passed, 4 warnings in 94.29s\n- Device: 0\n- Task: task_20260713_221343_20357588630\n\nThe earlier local tbe error was caused by overwriting CANN PYTHONPATH; preserving it as PYTHONPATH=/data/zhaomin/pypto-serving:$PYTHONPATH resolves the environment issue.

@zmnobug
zmnobug force-pushed the topk-sampling-optimization branch 2 times, most recently from fe17c71 to dcf67a7 Compare July 15, 2026 03:35
@zmnobug
zmnobug force-pushed the topk-sampling-optimization branch 5 times, most recently from b9493ee to e0895a7 Compare July 20, 2026 03:12
high-cloud pushed a commit to hw-native-sys/pypto-lib that referenced this pull request Jul 21, 2026
## Summary

Follow up on #769 by replacing the Qwen3-14B per-chunk Top-4
approximation with a provably exact full-vocabulary Top-32 selector.

## What Changed

- Split the 151936 real-vocabulary logits into 74 full 2048-token groups
and one 384-token tail group.
- Compute an exact Top-32 for each group with `sort32` and `mrgsort`.
- Merge the resulting `75 x 32 = 2400` candidates in a 4096-entry padded
buffer and select the exact global Top-32.
- Keep padded-vocabulary entries excluded with valid-shape masking and
minimum-value fill.
- Change the golden implementation to compare directly against
`torch.topk(logits[:, :REAL_VOCAB], 32)`.
- Add adversarial fixtures with all Top-32 values concentrated in one
512-token region and distributed across 32 regions.

If an entry is not in its group's Top-32, at least 32 entries in that
group rank ahead of it, so it cannot belong to the global Top-32.
Therefore the union of exact group Top-32 sets contains the exact global
Top-32.

## Validation

- A2/A3 NPU golden comparison: passed for values and token IDs.
- Greedy `selection_k=1` regression: passed.
- Standalone selector, 100 rounds: min 427.1 us, median 427.8 us, mean
428.0 us, max 429.9 us.
- Serving sampling tests after rebase: 29 passed.
- Shared-L3 batch-16 prefill/decode warmup: passed.
- Qwen3-14B 20-token top-k generation: 0.769 s E2E, 26.01 tok/s.

Paired serving PR:
hw-native-sys/pypto-serving#77

Co-authored-by: zmnobug <zmnobug@users.noreply.github.com>
@zmnobug
zmnobug force-pushed the topk-sampling-optimization branch from e0895a7 to 16f1e85 Compare July 29, 2026 02:34
@bumble0918

Copy link
Copy Markdown
Collaborator
  1. pypto_serving/model/qwen/npu_runner.py:640-672:top-k decode 路径仍然把 full logits 写回 host buffer 并返
    回 .cpu()。现在只有 device_greedy 会走 _decode_logits_device_arg() 跳过整词表 D2H;allow_device_topk_sampling=True 时 selector 虽然只返回 32 个候选,但 decode logits 仍是 host-visible full vocab。建议把 “device top-k” 也纳入 device-resident logits 路径:decode 输出写到 device scratch,topk_select 直接读 device scratch,DecodeResult.logits=None,只返回 SamplingCandidates。

  2. pypto_serving/serving/engine/engine.py:546-559 和 pypto_serving/serving/server/serving_worker.py:481-484:消费 sampling_candidates 没有再次按当前请求配置 gate。当前 Qwen runner 会在 allow=False 时返回 None,所以现有实现大概率正常;但接口层最好和 sampled_token_ids 一样显式受 allow_device_topk_sampling 控制,避免其他 executor 或 stale result 在 top_k=None、top_k>32、temperature<=0 时误用候选集。

  3. 测试覆盖还缺 serving worker 的 top-k 路由。tests/test_batching.py 覆盖了离线 LLMEngine.generate_batch,但没有覆盖 ServingWorker._batch_prefill/_batch_decode 对 allow_device_topk_sampling、fallback、不同 request 参数混批的行为。建议补一个 fake executor/fake scheduled batch 测试,至少验证 supported top-k 用 candidates,unsupported top-k 回落 full logits。

@zmnobug
zmnobug force-pushed the topk-sampling-optimization branch from 16f1e85 to 3658ed8 Compare July 30, 2026 07:40
@zmnobug

zmnobug commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

已在 3658ed8 中处理这三点:

  1. Top-k decode 现在与 device greedy 共用 device-resident logits/next-hidden scratch;topk_select 直接读取该 scratch,DecodeResult.logits=None,不再回传 full-vocab logits。
  2. LLMEngineWorkerProcess 在消费 sampling_candidates 时都会再次检查 allow_device_topk_sampling,未授权或 stale candidates 会走 full-logits fallback。
  3. 增加了 runner device-scratch 行为测试,以及 serving worker 的 supported Top-k、unsupported/mixed-batch fallback 测试。

验证结果:Ruff/compile checks 通过;相关测试 61 passed, 1 deselected

@zmnobug
zmnobug force-pushed the topk-sampling-optimization branch from 3658ed8 to 553252f Compare July 30, 2026 12:18
@superxf
superxf merged commit ed55def into hw-native-sys:main Jul 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Extend Qwen3 device sampling to support top-k

3 participants